﻿using System;
using System.Collections;
using System.Collections.Generic;

namespace EnterpriseLibraryExtensions.Services.Mapping
{
    /// <summary>
    /// The <see cref="EnterpriseLibraryExtensions.Services.Mapping.EntityTranslatorService"/> class is the entry point for creating an
    /// Entity Translator Service. The class works by having individual translators registered on it and then translate from any instance
    /// to a specific target type by using the built-in translators.
    /// </summary>
    public class EntityTranslatorService : IEntityTranslatorService
    {
        private readonly List<IEntityTranslator> _translators;
        private readonly IEntityTranslatorCollectionStrategyFactory _collectionTranslatorStrategyFactory;
        private readonly ICollectionElementTypeExtractor _collectionElementTypeExtractor;

        /// <summary>
        /// Creates a new instance of the <see cref="EnterpriseLibraryExtensions.Services.Mapping.EntityTranslatorService"/> class.
        /// </summary>
        public EntityTranslatorService()
            : this(new EntityTranslatorCollectionStrategyFactory(), new CollectionElementTypeExtractor())
        {
        }

        internal EntityTranslatorService(
            IEntityTranslatorCollectionStrategyFactory collectionTranslatorStrategyFactory,
            ICollectionElementTypeExtractor collectionElementTypeExtractor)
        {
            if (collectionTranslatorStrategyFactory == null)
            {
                throw new ArgumentNullException("collectionTranslatorStrategyFactory");
            }

            if (collectionElementTypeExtractor == null)
            {
                throw new ArgumentNullException("collectionElementTypeExtractor");
            }

            _collectionTranslatorStrategyFactory = collectionTranslatorStrategyFactory;
            _collectionElementTypeExtractor = collectionElementTypeExtractor;
            _translators = new List<IEntityTranslator>();
        }

        /// <summary>
        /// Register a new <paramref name="entityTranslator"/> in the service.
        /// </summary>
        /// <param name="entityTranslator">A valid entityTranslator.</param>
        /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="entityTranslator"/> is null.</exception>"
        public void RegisterEntityTranslator(IEntityTranslator entityTranslator)
        {
            if (entityTranslator == null)
            {
                throw new ArgumentNullException("entityTranslator");
            }

            _translators.Add(entityTranslator);
        }

        /// <summary>
        /// Removes a translator already registered in the service.
        /// </summary>
        /// <param name="entityTranslator">A valid, already registered translator.</param>
        /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="entityTranslator"/> is null.</exception>
        public void RemoveEntityTranslator(IEntityTranslator entityTranslator)
        {
            if (entityTranslator == null)
            {
                throw new ArgumentNullException("entityTranslator");
            }

            _translators.Remove(entityTranslator);
        }

        /// <summary>
        /// Returns whether or not the service is capable of translating betweeen the two provided types with the 
        /// currently registered translators.
        /// </summary>
        /// <typeparam name="TTarget">The type to translate to.</typeparam>
        /// <typeparam name="TSource">The type to translate from.</typeparam>
        /// <returns>True if translation is possible. Otherwise false.</returns>
        public bool CanTranslate<TTarget, TSource>()
        {
            return CanTranslate(typeof(TTarget), typeof(TSource));
        }

        /// <summary>
        /// Returns whether or not the service is capable of translating betweeen the two provided types with the 
        /// currently registered translators.
        /// </summary>
        /// <param name="targetType">The type to translate to.</param>
        /// <param name="sourceType">The type to translate from.</param>
        /// <returns>True if translation is possible. Otherwise false.</returns>
        /// <exception cref="System.ArgumentNullException">Thrown if either <paramref name="targetType"/> or <paramref name="sourceType"/> are null.</exception>
        public bool CanTranslate(Type targetType, Type sourceType)
        {
            if (targetType == null)
            {
                throw new ArgumentNullException("targetType");
            }

            if (sourceType == null)
            {
                throw new ArgumentNullException("sourceType");
            }

            return CollectionTranslationPossibleForTypes(targetType, sourceType) || FindTranslator(targetType, sourceType) != null;
        }

        /// <summary>
        /// Translate the source instance into the type specified by the generic argument TTarget.
        /// </summary>
        /// <typeparam name="TTarget">The desired target type.</typeparam>
        /// <param name="source">The source instance.</param>
        /// <returns>The translated instance.</returns>
        /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="source"/> is null.</exception>
        /// <exception cref="System.InvalidOperationException">Thrown if no translators can handle the translation.</exception>
        public TTarget Translate<TTarget>(object source)
        {
            return (TTarget)Translate(typeof(TTarget), source);
        }

        /// <summary>
        /// Translate the <paramref name="source"/> instance into the type specified by the <paramref name="targetType"/> parameter.
        /// </summary>
        /// <param name="targetType">The desired target type.</param>
        /// <param name="source">The source instance.</param>
        /// <returns>The translated instance.</returns>
        /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="source"/> or <paramref name="targetType"/> are null.</exception>
        /// <exception cref="System.InvalidOperationException">Thrown if no translators can handle the translation.</exception>
        public object Translate(Type targetType, object source)
        {
            if (targetType == null)
            {
                throw new ArgumentNullException("targetType");
            }

            if (source == null)
            {
                throw new ArgumentNullException("source");
            }

            Type sourceType = source.GetType();

            if (CollectionTranslationPossibleForTypes(targetType, sourceType))
            {
                return TranslateCollection(targetType, source);
            }

            IEntityTranslator translator = FindTranslator(targetType, sourceType);

            if (translator != null)
            {
                return translator.Translate(this, targetType, source);
            }

            throw new InvalidOperationException("No entityTranslator is available to perform the operation.");
        }

        private object TranslateCollection(Type targetType, object sourceInstance)
        {
            var strategy = _collectionTranslatorStrategyFactory.CreateCollectionHandler(targetType, sourceInstance);

            if (strategy == null)
                throw new InvalidOperationException("Unable to translate between the two types.");

            Type targetItemType = _collectionElementTypeExtractor.GetElementType(targetType);

            var sourceEnumerable = (IEnumerable)sourceInstance;

            foreach (var sourceElement in sourceEnumerable)
            {
                var translatedElement = Translate(targetItemType, sourceElement);
                strategy.AddElement(translatedElement);
            }

            return strategy.CreateInstance();
        }

        private bool CollectionTranslationPossibleForTypes(Type targetType, Type sourceType)
        {
            Type sourceElementType = _collectionElementTypeExtractor.GetElementType(sourceType);
            Type targetElementType = _collectionElementTypeExtractor.GetElementType(targetType);

            if (sourceElementType == null || targetElementType == null)
                return false;

            return CanTranslate(targetElementType, sourceElementType);
        }

        private IEntityTranslator FindTranslator(Type targetType, Type sourceType)
        {
            IEntityTranslator translator = _translators.Find(delegate(IEntityTranslator test)
            {
                return test.CanTranslate(targetType, sourceType);
            });

            return translator;
        }
    }
}
